package com.lagou.web.framework.mvc.servlet;

import com.lagou.web.framework.annotation.config.stereotype.Controller;
import com.lagou.web.framework.annotation.factory.BeanFactory;
import com.lagou.web.framework.mvc.config.Handler;
import com.lagou.web.framework.mvc.config.annotations.RequestMapping;
import com.lagou.web.framework.mvc.config.annotations.Security;
import org.apache.commons.lang3.StringUtils;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class DispatcherServlet extends HttpServlet {
    private List<Handler> handlerMappings = new ArrayList<>();

    @Override
    public void init() throws ServletException {
        initHandlerMapping();
    }

    private void initHandlerMapping() {
        Map<String, Object> singletonMap = BeanFactory.getSingletonMap();
        for (Object bean : singletonMap.values()) {
            Class<?> beanClass = bean.getClass();
            String baseUrl = "";
            if (beanClass.isAnnotationPresent(Controller.class) && beanClass.isAnnotationPresent(RequestMapping.class)) {
                RequestMapping requestMapping = beanClass.getAnnotation(RequestMapping.class);
                baseUrl = requestMapping.value();
            }
            Method[] methods = beanClass.getMethods();
            for (Method method : methods) {
                if (method.isAnnotationPresent(RequestMapping.class)) {
                    RequestMapping requestMapping = method.getAnnotation(RequestMapping.class);
                    String subUrl = requestMapping.value();
                    Handler handler = new Handler(bean, method, Pattern.compile((baseUrl.startsWith("/") ? baseUrl : ("/" + baseUrl)) + (subUrl.startsWith("/") ? subUrl : ("/" + subUrl))));
                    Parameter[] parameters = method.getParameters();
                    for (int i = 0; i < parameters.length; i++) {
                        handler.getParameterIndexMap().put(parameters[i], i);
                    }
                    handlerMappings.add(handler);
                }
            }
        }
    }

    private Handler getHandler(HttpServletRequest req) {
        String requestURI = req.getRequestURI();
        for (Handler handler : handlerMappings) {
            Matcher matcher = handler.getUrl().matcher(requestURI);
            if (matcher.matches()) {
                return handler;
            }
        }
        return null;
    }

    private Boolean applyPreHandle(HttpServletRequest req, HttpServletResponse resp, Handler handler) throws ServletException, IOException {
        Method method = handler.getMethod();
        Class<?> controllerClass = handler.getController().getClass();
        if (method.isAnnotationPresent(Security.class) || controllerClass.isAnnotationPresent(Security.class)) {
            List<String> securityValues = new ArrayList<>();
            Security security = method.getAnnotation(Security.class);
            String[] value = null;
            if (security == null || (value = security.value()) == null) {
                security = controllerClass.getAnnotation(Security.class);
                if (security != null) {
                    value = security.value();
                }
            }
            if (value != null && value.length > 0) {
                securityValues.addAll(Arrays.asList(value));
            }
            String[] userNames = req.getParameterValues("username");
            if (userNames != null && userNames.length > 0) {
                for (String userName : userNames) {
                    if (securityValues.indexOf(userName) > -1) {
                        return true;
                    }
                }
            }
            req.getRequestDispatcher("/WEB-INF/jsp/error.jsp").forward(req, resp);
            return false;
        }
        return true;
    }

    private Object handle(HttpServletRequest req, HttpServletResponse resp, Handler handler) throws UnsupportedEncodingException {
        Map<Parameter, Integer> parameterIndexMap = handler.getParameterIndexMap();
        Object[] args = new Object[parameterIndexMap.size()];
        for (Map.Entry<Parameter, Integer> entry : parameterIndexMap.entrySet()) {
            Parameter parameter = entry.getKey();
            Integer parameterIndex = entry.getValue();
            Class<?> parameterType = parameter.getType();
            String[] parameterValueArray = req.getParameterValues(parameter.getName());
            String parameterValues = StringUtils.join(parameterValueArray, ",");
            if (parameterType == HttpServletRequest.class) {
                args[parameterIndex] = req;
            } else if (parameterType == HttpServletResponse.class) {
                args[parameterIndex] = resp;
            } else if (parameterType == String[].class) {
                args[parameterIndex] = parameterValueArray;
            } else if (parameterType == String.class) {
                args[parameterIndex] = parameterValues;
            } else if (parameterType == Byte.class) {
                args[parameterIndex] = Byte.valueOf(parameterValues);
            } else if (parameterType == Short.class) {
                args[parameterIndex] = Short.valueOf(parameterValues);
            } else if (parameterType == Integer.class) {
                args[parameterIndex] = Integer.valueOf(parameterValues);
            } else if (parameterType == Long.class) {
                args[parameterIndex] = Long.valueOf(parameterValues);
            } else if (parameterType == Float.class) {
                args[parameterIndex] = Float.valueOf(parameterValues);
            } else if (parameterType == Double.class) {
                args[parameterIndex] = Double.valueOf(parameterValues);
            } else if (parameterType == Boolean.class) {
                args[parameterIndex] = Boolean.valueOf(parameterValues);
            } else if (parameterType == Character.class) {
                args[parameterIndex] = Character.valueOf(StringUtils.isNotBlank(parameterValues) ? parameterValues.charAt(0) : '0');
            } else {
                args[parameterIndex] = null;
            }
        }
        try {
            return handler.getMethod().invoke(handler.getController(), args);
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }
        return null;
    }

    private void processDispatchResult(String dispatcherUrl, HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        req.getRequestDispatcher(dispatcherUrl).forward(req, resp);
    }

    private void doDispatch(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException {
        Handler handler = getHandler(req);
        if (handler == null) {
            resp.getWriter().write("404 not found");
            return;
        }
        if (!applyPreHandle(req, resp, handler)) {
            return;
        }
        Object result = handle(req, resp, handler);
        if (result != null && result.getClass() == String.class) {
            processDispatchResult((String) result, req, resp);
        }
    }

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        doPost(req, resp);
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        doDispatch(req, resp);
    }
}
